%%%-------------------------------------------------------------------
%%% File    : protobuffs_eqc.erl
%%% Author  : David AAberg <david_ab@RB-DAVIDAB01>
%%% Description :
%%%
%%% Created :  5 Aug 2010 by David AAberg <david_ab@RB-DAVIDAB01>
%%%-------------------------------------------------------------------
-module(protobuffs_proper).

-include("quickcheck_setup.hrl").

-compile(export_all).

utf8char() ->
    union([integer(0, 36095), integer(57344, 65533),
       integer(65536, 1114111)]).

utf8string() -> list(utf8char()).

uint32() -> choose(0, 4294967295).

sint32() -> choose(-2147483648, 2147483647).

uint64() -> choose(0, 18446744073709551615).

sint64() ->
    choose(-9223372036854775808, 9223372036854775807).

value() ->
    oneof([{real(), double}, {real(), float}, {nan, float},
       {infinity, float}, {'-infinity', float}, {nan, double},
       {infinity, double}, {'-infinity', double},
       {uint32(), uint32}, {uint64(), uint64},
       {sint32(), sint32}, {sint64(), sint64},
       {uint32(), fixed32}, {uint64(), fixed64},
       {sint32(), sfixed32}, {sint64(), sfixed64},
       {sint32(), int32}, {sint64(), int64}, {bool(), bool},
       {sint32(), enum}, {utf8string(), string},
       {binary(), bytes}]).

compare_messages(ExpectedMsg, Msg) ->
    lists:foldl(fun ({E, D}, Acc) ->
            compare(E, D) andalso Acc
        end,
        true,
        lists:zip(tuple_to_list(ExpectedMsg),
              tuple_to_list(Msg))).

compare(A, A) -> true;
compare([A], B) -> compare(A, B);
compare(A, [B]) -> compare(A, B);
compare(A, B) when is_tuple(A), is_tuple(B) ->
    compare(tuple_to_list(A), tuple_to_list(B));
compare([A | RA], [B | RB]) ->
    compare(A, B) andalso compare(RA, RB);
compare(A, B) when is_float(A), is_float(B) ->
    <<A32:32/little-float>> = <<A:32/little-float>>,
    <<B32:32/little-float>> = <<B:32/little-float>>,
    if A =:= B -> true;
       A32 =:= B32 -> true;
       true -> false
    end;
compare(A, B) ->
    error_logger:error_msg("~p =/= ~p~n", [A, B]), false.

prop_protobuffs() ->
    ?FORALL({FieldID, {Value, Type}},
        {?SUCHTHAT(I, (uint32()), (I =< 1073741823)), value()},
        begin
          case Type of
        float when is_float(Value) ->
            Encoded = iolist_to_binary(protobuffs:encode(FieldID, Value, Type)),
            {{FieldID, Float}, <<>>} = protobuffs:decode(Encoded,
                                 Type),
            <<Value32:32/little-float>> = <<Value:32/little-float>>,
            Float =:= Value32;
        _Else ->
            Encoded = iolist_to_binary(protobuffs:encode(FieldID, Value, Type)),
            {{FieldID, Value}, <<>>} ==
              protobuffs:decode(Encoded, Type)
          end
        end).

prop_protobuffs_packed() ->
    ?FORALL({FieldID, {Values, Type}},
        {?SUCHTHAT(I, (uint32()), (I =< 1073741823)),
         oneof([{non_empty(list(uint32())), uint32},
            {non_empty(list(uint64())), uint64},
            {non_empty(list(sint32())), sint32},
            {non_empty(list(sint64())), sint64},
            {non_empty(list(sint32())), int32},
            {non_empty(list(sint64())), int64},
            {non_empty(list(bool())), bool},
            {non_empty(list(real())), double},
            {non_empty(list(real())), float}])},
        begin
          case Type of
        float ->
            Encoded = iolist_to_binary(protobuffs:encode_packed(FieldID, Values,
                               Type)),
            {{FieldID, DecodedValues}, <<>>} =
            protobuffs:decode_packed(Encoded, Type),
            lists:all(fun ({Expected, Result}) ->
                      <<Expected32:32/little-float>> =
                      <<Expected:32/little-float>>,
                      Expected32 =:= Result
                  end,
                  lists:zip(Values, DecodedValues));
        _Else ->
            Encoded = iolist_to_binary(protobuffs:encode_packed(FieldID, Values,
                               Type)),
            Decoded = protobuffs:decode_packed(Encoded, Type),
            {{FieldID, Values}, <<>>} == Decoded
          end
        end).

proper_protobuffs_empty() ->
    ?FORALL({Empty},
        {{empty, default(undefined, real()),
          default(undefined, real()),
          default(undefined, sint32()),
          default(undefined, sint64()),
          default(undefined, uint32()),
          default(undefined, uint64()),
          default(undefined, sint32()),
          default(undefined, sint64()),
          default(undefined, uint32()),
          default(undefined, uint64()),
          default(undefined, sint32()),
          default(undefined, sint64()),
          default(undefined, bool()),
          default(undefined, utf8string()),
          default(undefined, binary()),
          default(undefined, {empty_emptymessage})}},
        begin
          Decoded =
          empty_pb:decode_empty(iolist_to_binary(empty_pb:encode_empty(Empty))),
          compare_messages(Empty, Decoded)
        end).

check_with_default(Expected, Result, undefined, Fun) ->
    Fun(Expected, Result);
check_with_default(undefined, Result, Default, Fun) ->
    Fun(Default, Result);
check_with_default(Expected, Result, _Default, Fun) ->
    Fun(Expected, Result).

proper_protobuffs_hasdefault() ->
    ?FORALL(Withdefault,
        {withdefault, {real(), 1.0}, {real(), 2.0},
         {sint32(), 1}, {sint64(), 2}, {uint32(), 3},
         {uint64(), 4}, {sint32(), 5},
         {default(undefined, sint64()), 6},
         {default(undefined, uint32()), 7},
         {default(undefined, uint64()), 8},
         {default(undefined, sint32()), 9},
         {default(undefined, sint64()), 10},
         {default(undefined, bool()), true},
         {default(undefined, utf8string()), "test"},
         {default(undefined, utf8string()), ""}},
        begin
          FunGetDefault = fun ({undefined, Val}) -> Val;
                  ({Val, _}) -> Val;
                  (Val) -> Val
                  end,
          FunGetTestMsg = fun ({Val, _}) -> Val;
                  (Val) -> Val
                  end,
          Expected = list_to_tuple(lists:map(FunGetDefault,
                         tuple_to_list(Withdefault))),
          TestMsg = list_to_tuple(lists:map(FunGetTestMsg,
                        tuple_to_list(Withdefault))),
          Decoded =
          hasdefault_pb:decode_withdefault(iolist_to_binary(hasdefault_pb:encode_withdefault(TestMsg))),
          compare_messages(Expected, Decoded)
        end).

location() ->
    Str = utf8string(),
    default(undefined, {location, Str, Str}).

proper_protobuffs_simple() ->
    ?FORALL({Person},
        {{person, utf8string(), utf8string(), utf8string(),
          sint32(), location()}},
        begin
          Decoded =
          simple_pb:decode_person(iolist_to_binary(simple_pb:encode_person(Person))),
          compare_messages(Person, Decoded)
        end).

phone_type() ->
    Int32 = default(undefined, sint32()),
    {person_phonenumber_phonetype, Int32, Int32, Int32}.

phone_number() ->
    {person_phonenumber, utf8string(),
     default(undefined, phone_type())}.

proper_protobuffs_nested1() ->
    ?FORALL(Person,
        {person, utf8string(), sint32(),
         default(undefined, utf8string()), list(phone_number())},
        begin
          Decoded =
          nested1_pb:decode_person(iolist_to_binary(nested1_pb:encode_person(Person))),
          compare_messages(Person, Decoded)
        end).

innerAA() ->
    {outer_middleaa_inner, sint64(),
     default(undefined, bool())}.

middleAA() ->
    Inner = innerAA(),
    {outer_middleaa, default(undefined, Inner)}.

innerBB() ->
    {outer_middlebb_inner, sint32(),
     default(undefined, bool())}.

middleBB() ->
    Inner = innerBB(),
    {outer_middlebb, default(undefined, Inner)}.

proper_protobuffs_nested2() ->
    ?FORALL({Middle},
        {{outer, default(undefined, middleAA()),
          default(undefined, middleBB())}},
        begin
          Decoded =
          nested2_pb:decode_outer(iolist_to_binary(nested2_pb:encode_outer(Middle))),
          compare_messages(Middle, Decoded)
        end).

inner() ->
    {outer_middle_inner, default(undefined, bool())}.

other() -> {outer_other, default(undefined, bool())}.

middle() ->
    Inner = inner(),
    Other = other(),
    {outer_middle, Inner, Other}.

proper_protobuffs_nested3() ->
    ?FORALL({Middle},
        {default({outer, undefined}, {outer, middle()})},
        begin
          Decoded =
          nested3_pb:decode_outer(iolist_to_binary(nested3_pb:encode_outer(Middle))),
          compare_messages(Middle, Decoded)
        end).

proper_protobuffs_nested4() ->
    ?FORALL({Middle},
        {default({outer, undefined}, {outer, middle()})},
        begin
          Decoded =
          nested4_pb:decode_outer(iolist_to_binary(nested4_pb:encode_outer(Middle))),
          compare_messages(Middle, Decoded)
        end).

first_inner() ->
    {first_inner, default(undefined, bool())}.

proper_protobuffs_nested5() ->
    ?FORALL(Inner,
        (oneof([default({first, undefined},
                {first, first_inner()}),
            {second, first_inner()}])),
        begin
          case element(1, Inner) of
        first ->
            Decoded =
            nested5_pb:decode_first(iolist_to_binary(nested5_pb:encode_first(Inner))),
            compare_messages(Inner, Decoded);
        second ->
            Decoded =
            nested5_pb:decode_second(iolist_to_binary(nested5_pb:encode_second(Inner))),
            compare_messages(Inner, Decoded)
          end
        end).

enum_value() -> oneof([value1, value2]).

proper_protobuffs_enum() ->
    ?FORALL({Middle},
        {default({enummsg, undefined},
             {enummsg, enum_value()})},
        begin
          Decoded =
          enum_pb:decode_enummsg(iolist_to_binary(enum_pb:encode_enummsg(Middle))),
          compare_messages(Middle, Decoded)
        end).

enum_outside_value() -> oneof(['FIRST', 'SECOND']).

proper_protobuffs_enum_outside() ->
    ?FORALL({Middle},
        {default({enumuser, undefined},
             {enumuser, enum_outside_value()})},
        begin
          Decoded =
          enum_outside_pb:decode_enumuser(iolist_to_binary(enum_outside_pb:encode_enumuser(Middle))),
          compare_messages(Middle, Decoded)
        end).

proper_protobuffs_extensions() ->
    ?FORALL({Middle},
        {default({extendable, dict:new()},
             {maxtendable, dict:new()})},
        begin
          DecodeFunc = list_to_atom("decode_" ++
                      atom_to_list(element(1, Middle))),
          Decoded =
          extensions_pb:DecodeFunc(iolist_to_binary(extensions_pb:encode(Middle))),
          compare_messages(Middle, Decoded)
        end).

address_phone_number() ->
    {person_phonenumber, utf8string(),
     oneof(['HOME', 'WORK', 'MOBILE'])}.

person() ->
    {person, utf8string(), sint32(),
     default(undefined, utf8string()),
     list(address_phone_number())}.

proper_protobuffs_addressbook() ->
    ?FORALL(Addressbook, {addressbook, list(person())},
        begin
          Decoded =
          addressbook_pb:decode_addressbook(iolist_to_binary(addressbook_pb:encode_addressbook(Addressbook))),
          compare_messages(Addressbook, Decoded)
        end).

repeater_location() ->
    {location, utf8string(), utf8string()}.

repeater_person() ->
    {person, utf8string(), utf8string(), utf8string(),
     sint32(), list(utf8string()), list(repeater_location()),
     list(uint32())}.

proper_protobuffs_repeater() ->
    ?FORALL(Repeater, (repeater_person()),
        begin
          Decoded =
          repeater_pb:decode_person(iolist_to_binary(repeater_pb:encode_person(Repeater))),
          compare_messages(Repeater, Decoded)
        end).

proper_protobuffs_packed_repeated() ->
    ?FORALL(Repeater, (repeater_person()),
        begin
          Decoded =
          packed_repeated_pb:decode_person(iolist_to_binary(packed_repeated_pb:encode_person(Repeater))),
          compare_messages(Repeater, Decoded)
        end).

special_words() ->
    {message, utf8string(), utf8string(), utf8string(),
     utf8string(), utf8string(), utf8string(), utf8string(),
     utf8string(), utf8string(), utf8string(), utf8string(),
     utf8string(), utf8string(), utf8string(), utf8string(),
     utf8string(), utf8string(), utf8string(), utf8string(),
     utf8string(), utf8string(), utf8string(), utf8string(),
     utf8string(), utf8string(), utf8string(), utf8string(),
     utf8string(), utf8string(), utf8string()}.

proper_protobuffs_special_words() ->
    ?FORALL({SpecialWords}, {special_words()},
        begin
          Decoded =
          special_words_pb:decode_message(iolist_to_binary(special_words_pb:encode_message(SpecialWords))),
          compare_messages(SpecialWords, Decoded)
        end).

proper_protobuffs_import() ->
    ?FORALL({Imported},
        {default({foo, {imported, utf8string()}},
             {foo, undefined})},
        begin
          Decoded =
          import_pb:decode_foo(iolist_to_binary(import_pb:encode(Imported))),
          compare_messages(Imported, Decoded)
        end).

single() -> {message, uint32()}.

proper_protobuffs_single() ->
    ?FORALL(Single, (single()),
        begin
          Decoded =
          single_pb:decode_message(iolist_to_binary(single_pb:encode_message(Single))),
          compare_messages(Single, Decoded)
        end).

proper_protobuffs_extend() ->
    ?FORALL(Extend,
        (default({extendable,
              dict:from_list([{126,
                       {optional, sint32(), sint32, []}}])},
             {extendable, dict:new()})),
        begin
          Decoded =
          extend_pb:decode_extendable(iolist_to_binary(extend_pb:encode_extendable(Extend))),
          compare_messages(Extend, Decoded)
        end).

proper_protobuffs_extend_degraded() ->
    ?FORALL(Extend,
        (default({extendable,
              dict:from_list([{126,
                       {optional, sint32(), sint32, []}}])},
             {extendable, dict:new()})),
        begin
          Encoded = extend_pb:encode_extendable(Extend),
          DegradedDecoded =
          extensions_pb:decode_extendable(iolist_to_binary(Encoded)),
          Decoded = extend_pb:decode_extensions(DegradedDecoded),
          compare_messages(Extend, Decoded)
        end).

proper_protobuffs_extend_assign() ->
    ?FORALL(Extend, (sint32()),
        begin
          Input = {extendable, dict:new()},
          Expected = {extendable,
              dict:from_list([{126,
                       {optional, Extend, sint32, none}}])},
          {ok, Output} = extend_pb:set_extension(Input, bar,
                             Extend),
          compare_messages(Expected, Output)
        end).

proper_protobuffs_assign_encode() ->
    ?FORALL(Extend, (sint32()),
        begin
          Input = {extendable, dict:new()},
          Expected = {extendable,
              dict:from_list([{126,
                       {optional, Extend, sint32, []}}])},
          {ok, _Middle} = extend_pb:set_extension(Input, bar,
                             Extend),
          Output =
          extend_pb:decode_extendable(extend_pb:encode_extendable(Expected)),
          compare_messages(Expected, Output)
        end).

proper_protobuffs_extend_get() ->
    ?FORALL(Extend, (sint32()),
        begin
          Input = {extendable, dict:new()},
          {ok, Middle} = extend_pb:set_extension(Input, bar,
                             Extend),
          Decoded =
          extend_pb:decode_extendable(iolist_to_binary(extend_pb:encode_extendable(Middle))),
          Output = extend_pb:get_extension(Decoded, bar),
          compare({ok, Extend}, Output)
        end).

proper_protobuffs_extend_has_enum() ->
    ?FORALL(Extend, (oneof(['FOO', 'BAR'])),
        begin
          Input = {extendable, dict:new()},
          {ok, Encodable} = extend_pb:set_extension(Input, baz,
                            Extend),
          Decoded =
          extend_pb:decode_extendable(iolist_to_binary(extend_pb:encode_extendable(Encodable))),
          Out = extend_pb:get_extension(Decoded, baz),
          compare({ok, Extend}, Out)
        end).

proper_protobuffs_extend_has_message() ->
    ?FORALL(Extend,
        (default({maxtendable, dict:new()},
             {maxtendable,
              dict:from_list([{505,
                       {optional, utf8string(), string,
                    []}}])})),
        begin
          Input = {extendable, dict:new()},
          {ok, Encodable} = extend_pb:set_extension(Input, bin,
                            Extend),
          Decoded =
          extend_pb:decode_extendable(iolist_to_binary(extend_pb:encode_extendable(Encodable))),
          Out = extend_pb:get_extension(Decoded, bin),
          compare({ok, Extend}, Out)
        end).

proper_protobuffs_extend_has_string() ->
    ?FORALL(Extend, (utf8string()),
        begin
          Input = {extendable, dict:new()},
          {ok, Encodable} = extend_pb:set_extension(Input,
                            stringy, Extend),
          Decoded =
          extend_pb:decode_extendable(iolist_to_binary(extend_pb:encode_extendable(Encodable))),
          Out = extend_pb:get_extension(Decoded, stringy),
          compare({ok, Extend}, Out)
        end).

proper_protobuffs_service() ->
    %Don't handel service tag for the moment testing no errors and that the messages works
    ?FORALL(Service,
        (oneof([{searchresponse,
             default(undefined, utf8string())},
            {searchrequest, default(undefined, utf8string())}])),
        begin
          case element(1, Service) of
        searchresponse ->
            Decoded =
            service_pb:decode_searchresponse(iolist_to_binary(service_pb:encode_searchresponse(Service))),
            compare_messages(Service, Decoded);
        searchrequest ->
            Decoded =
            service_pb:decode_searchrequest(iolist_to_binary(service_pb:encode_searchrequest(Service))),
            compare_messages(Service, Decoded)
          end
        end).

proper_protobuffs_exports() ->
    ?FORALL(Exporter,
        (oneof([{exporter, sint32()},
            {export_this_message, sint32()}])),
        begin
            Decoded = case element(1, Exporter) of
                exporter ->
                    Encoded = exports_pb:encode_exporter(Exporter),
                    exports_pb:decode_exporter(iolist_to_binary(Encoded));
                export_this_message ->
                    Encoded = exports_pb:encode_export_this_message(Exporter),
                    exports_pb:decode_export_this_message(iolist_to_binary(Encoded))
            end,
            compare_messages(Exporter, Decoded)
        end).

proper_protobuffs_imports() ->
    ?FORALL(Importer,
        (oneof([
            {importer, {exporter, sint32()}},
            {import_with_underscores, {export_this_message, sint32()}},
            {exporter, sint32()},
            {export_this_message, sint32()}])),
        begin
            Decoded = case element(1, Importer) of
                importer ->
                    Encoded = imports_pb:encode_importer(Importer),
                    imports_pb:decode_importer(iolist_to_binary(Encoded));
                import_with_underscores ->
                    Encoded = imports_pb:encode_import_with_underscores(Importer),
                    imports_pb:decode_import_with_underscores(iolist_to_binary(Encoded));
                exporter ->
                    Encoded = imports_pb:encode_exporter(Importer),
                    imports_pb:decode_exporter(iolist_to_binary(Encoded));
                export_this_message ->
                    Encoded = imports_pb:encode_export_this_message(Importer),
                    imports_pb:decode_export_this_message(iolist_to_binary(Encoded))
            end,
            compare_messages(Importer, Decoded)
        end).

proper_protobuffs_camel_case() ->
    ?FORALL(Record,
    ({outer, oneof([utf8string(), undefined]), list({inner, oneof([utf8string(), undefined])})}),
    begin
        Decoded = camel_case_pb:decode_outer(iolist_to_binary(camel_case_pb:encode_outer(Record))),
        compare_messages(Record, Decoded)
    end).

chunk_up_binary(Binary, Size) ->
    chunk_up_binary(Binary, Size, []).

chunk_up_binary(<<>>, _Size, Acc) ->
    lists:reverse(Acc);

chunk_up_binary(Binary, Size, Acc) when size(Binary) =< Size ->
    lists:reverse([Binary | Acc]);

chunk_up_binary(Binary, Size, Acc) ->
    <<Chunk:Size/binary, Rest/binary>> = Binary,
    chunk_up_binary(Rest, Size, [Chunk | Acc]).

proper_protobuffs_delimed() ->
    ?FORALL({Records, ChunkSize}, {list({delimed, utf8string(), int()}), non_neg_integer()}, begin
        Encoded = delimed_pb:encode(Records),
        EncodedBin = iolist_to_binary(Encoded),
        Binaries = chunk_up_binary(EncodedBin, ChunkSize + 1),
        {Decoded, Rest} = lists:foldl(fun(Binary, {RecAcc, Buffer}) ->
            {AppendRec, NewBuffer} = delimed_pb:delimited_decode_delimed(<<Buffer/binary, Binary/binary>>),
            {RecAcc ++ AppendRec, NewBuffer}
        end, {[], <<>>}, Binaries),
        if
            length(Records) == length(Decoded) ->
                lists:all(fun({Expected, Got}) ->
                    Out = compare_messages(Expected, Got),
                    case Out of
                        false ->
                            test_server:format("comparison failed:~n"
                                "    Expected: ~p~n"
                                "    Got: ~p", [Expected, Got]);
                        true ->
                            ok
                    end,
                    Out
                end, lists:zip(Records, Decoded));
            true ->
                test_server:format("Different number of returned records: ~p vs ~p~n"
                    "    Records: ~p~n"
                    "    Decoded: ~p~n"
                    "    Binary Left: ~p", [length(Records), length(Decoded), Records, Decoded, Rest]),
                false
        end
    end).
